查看原文
其他

吴章金: 实例解析 Linux C 语言程序之变量类型

吴章金 Linux阅码场 2022-12-14

license: "cc-by-nc-nd-4.0"  

"本文从编译、二进制程序文件和运行角度逐级解析了 Linux C 语言程序中几种变量类型" 

吴章金老师《360度剖析Linux ELF系列文章》:

吴章金:如何创建一个*可执行*的共享库

吴章金: 深度剖析 Linux共享库的“位置无关”实现原理

吴章金:通过操作 Section 为 Linux ELF 程序新增数据

背景说明

前几天,有同学在 “泰晓原创团队” 讨论群问道:

请教下,谭 C,8.9.3,用 static 声明静态局部变量,在实际中可有案例。

看到这个问题,立即浮现的概念是 RUN ONCE,内核源码找了一下:

  1. $ grep -i "static.*run_once" -ur ./ --include "*.c"

  2. ./arch/mips/mm/page.c: static atomic_t run_once = ATOMIC_INIT(0);

  3. ./arch/mips/mm/page.c: static atomic_t run_once = ATOMIC_INIT(0);

  4. ./arch/mips/mm/tlbex.c: static int run_once = 0;

代码示例1:

  1. void build_clear_page(void)

  2. {

  3. static atomic_t run_once = ATOMIC_INIT(0);


  4. if (atomic_xchg(&run_once, 1)) {

  5. return;

  6. }

  7. /* body */

  8. }

代码示例2:

  1. void build_tlb_refill_handler(void)

  2. {

  3. ...

  4. if (!run_once) {

  5. /* body */

  6. run_once++;

  7. }

  8. }

另外,他又继续问道:

一般是用 static 定义一个全局的,很少看到函数内部在用 static?

这个问题则上升到 C 语言关键字 static 的用法。

static 这个关键字用来限定某个变量或者函数的作用域,这个作用域可能是文件层面,也可能是函数层面。

从语法的角度去解释某个关键字用法的文章很多,可是这些解释蛮多时候是很生硬的,不是那么好记忆。

本文尝试从实操的角度去解析 static 以及更多类型的 C 语言变量的形态。

从编译的角度

假如某个功能需求由多个文件构成如下:

  1. $ cat print.h

  2. extern void print(char *str);

  3. extern char *hello;


  4. $ cat hello.c

  5. #include "print.h"


  6. int main(void)

  7. {

  8. print(hello);

  9. return 0;

  10. }


  11. $ cat print.c

  12. #include <stdio.h>


  13. char *hello = "hello";


  14. void print(char *str);

  15. {

  16. printf("%s\n", str);

  17. }

编译运行如下:

  1. $ gcc -m32 -o hello x.c print.h print.c

  2. $ ./hello

  3. hello

类似这种需要跨文件访问的函数和变量,如果定义成 static 的话:

  1. $ cat print.c

  2. #include <stdio.h>


  3. static char *hello = "hello";


  4. static void print(char *str);

  5. {

  6. printf("%s\n", str);

  7. }


  8. $ gcc -m32 -o hello x.c print.h print.c

  9. /tmp/ccetJaG2.o: In function `main':

  10. x.c:(.text+0x12): undefined reference to `hello'

  11. x.c:(.text+0x1b): undefined reference to `print'

  12. collect2: error: ld returned 1 exit status

从 ELF 二进制程序文件的角度

先来编译成一个中间的可重定位文件:

  1. $ gcc -m32 -c -o print.o print.c

针对加 static 的情况:

  1. $ readelf -s print.o | egrep "hello$|print$"

  2. 6: 00000000 4 OBJECT LOCAL DEFAULT 3 hello

  3. 7: 00000000 23 FUNC LOCAL DEFAULT 1 print

不加的情况:

  1. $ readelf -s print.o | egrep "hello$|print$"

  2. 9: 00000000 4 OBJECT GLOBAL DEFAULT 3 hello

  3. 10: 00000000 23 FUNC GLOBAL DEFAULT 1 print

LOCALGLOBAL 直观地反应了 static 用于限定变量和函数在文件之外是否可访问。加了 static 以后,文件之外不可见。

补充另外一个 nm 工具的结果,针对加 static 的情况:

  1. $ nm print.o | egrep "hello$|print$"

  2. 00000000 d hello

  3. 00000000 t print

不加的情况:

  1. $ nm print.o | egrep "hello$|print$"

  2. 00000000 D hello

  3. 00000000 T print

上面四个字母有两组大小写,分别对应 data, text 的 LOCAL 和 GOLOBAL 符号,其中 "hello" 是数据,“print” 作为函数处在代码区域。

man nm:

"D" "d" The symbol is in the initialized data section.

"T" "t" The symbol is in the text (code) section.

If lowercase, the symbol is usually local; if uppercase, the symbol is global (external). There are however a few lowercase symbols that are shown for special global symbols ("u", "v" and "w")

延伸介绍到 nm 这个工具是因为,Linux 内核的 System.map 这样的符号表文件经常会被用来调试,这个文件实际上是用 nm 导出来的。

再延伸一个 WEAK 类型,这个类型类似于不加 static 的 GLOBAL,但是呢,允许定义另外一个同名的函数或者变量,用来覆盖 WEAK 类型的这个:

  1. $ cat print.c

  2. #include <stdio.h>


  3. __attribute__((weak)) char *hello = "hello";


  4. __attribute__((weak)) void print(char *str)

  5. {

  6. printf("%s\n", str);

  7. }


  8. $ cat hello.c

  9. #include "print.h"


  10. char *hello = "hello, world";


  11. int main(void)

  12. {

  13. print(hello);

  14. return 0;

  15. }


  16. $ ./hello

  17. hello, world

这种情况允许某个变量或者函数的“multiple definition”,如果不定义为 WEAK 类型而且不定义为 LOCAL(用 static),这种情况本来是不被允许的:

  1. $ gcc -m32 -o hello x.c print.h print.c

  2. /tmp/ccMO5y0A.o:(.data+0x0): multiple definition of `hello'

  3. /tmp/ccj8KK1s.o:(.data+0x0): first defined here

  4. collect2: error: ld returned 1 exit status

这种用法在内核中被广泛采用,通常用来确保可以添加架构特定的优化函数:

  1. $ grep __weak -ur ./ --include "*.c" | wc -l

  2. 413

汇总如下:

关键字类型说明
staticLOCAL限文件内访问
不加 staticGLOBAL文件外可在 extern 声明后访问
weak attributeWEAK同 GLOBAL,但可重定义

从运行的角度

上面从编译和二进制程序文件的角度分析了 static 关键字针对文件层面变量和函数的约定,下面再来看看函数内部的变量,在声明为 static 与否情况下的异同。

作为对比,把其他类型的变量也纳入进来:

  1. $ cat hello.c

  2. #include <stdio.h>


  3. static int m;

  4. static int n = 1000;

  5. int a;

  6. int b = 10000;


  7. static int hello(void)

  8. {

  9. static int i;

  10. static int j = 10;

  11. int x;

  12. int y = 100;

  13. register int z = 33;


  14. printf("i = %d, addr of i = %p\n", i, &i);

  15. printf("j = %d, addr of j = %p\n", j, &j);

  16. printf("x = %d, addr of x = %p\n", x, &x);

  17. printf("y = %d, addr of y = %p\n", y, &y);

  18. printf("z = %d, in register, no addr\n", z);


  19. return 0;

  20. }


  21. int main(int argc, char *argv[])

  22. {

  23. printf("argc = %d, addr of argc = %p\n", argc, &argc);

  24. printf("argv = %s, addr of argv = %p\n", argv[0], argv);

  25. printf("m = %d, addr of m = %p\n", m, &m);

  26. printf("n = %d, addr of n = %p\n", n, &n);

  27. printf("a = %d, addr of a = %p\n", a, &a);

  28. printf("b = %d, addr of b = %p\n", b, &b);


  29. hello();


  30. return 0;

  31. }


  32. $ gcc -m32 -o hello hello.c

  33. $ ./hello

  34. argc = 1, addr of argc = 0xffd91f60

  35. argv = ./hello, addr of argv = 0xffd91ff4

  36. m = 0, addr of m = 0x804a030

  37. n = 1000, addr of n = 0x804a020

  38. a = 0, addr of a = 0x804a038

  39. b = 10000, addr of b = 0x804a024

  40. i = 0, addr of i = 0x804a034

  41. j = 10, addr of j = 0x804a028

  42. x = -143124200, addr of x = 0xffd91f24

  43. y = 100, addr of y = 0xffd91f28

  44. z = 33, in register, no addr

用二进制程序文件来佐证:

  1. $ readelf -S hello | grep 804a | tail -2

  2. [24] .data PROGBITS 0804a018 001018 000014 00 WA 0 0 4

  3. [25] .bss NOBITS 0804a02c 00102c 000010 00 WA 0 0 4


  4. $ readelf -s hello | egrep " m$| n$| a$| b$| i| j| x$| y$"

  5. 36: 0804a030 4 OBJECT LOCAL DEFAULT 25 m

  6. 37: 0804a020 4 OBJECT LOCAL DEFAULT 24 n

  7. 39: 0804a034 4 OBJECT LOCAL DEFAULT 25 i.2021

  8. 40: 0804a028 4 OBJECT LOCAL DEFAULT 24 j.2022

  9. 54: 0804a024 4 OBJECT GLOBAL DEFAULT 24 b

  10. 67: 0804a038 4 OBJECT GLOBAL DEFAULT 25 a

综合上面 3 组数据:

类型代码存储区域
文件内static int m;.bss(被初始化为 0)
文件内static int n=1000;.data
文件内int a;.bss(被初始化为 0), GLOBAL
文件内int b = 10000;.data, GLOBAL
函数内static int i;.bss(被初始化为 0)
函数内static int j = 10;.data
函数内int x;stack(值随机,未初始化)
函数内int y = 100;stack
函数内register int z = 33;register(汇编中分配好)
函数参数argc, argvstack(默认调用约定)

再补充几点:

  1. 用 register 定义的变量存放在寄存器中,所以无法获取它们的内存地址(因为根本不存放在内存中)。可以通过查看汇编代码确认:



    1. $ gcc -m32 -S -o hello.s hello.c

    2. $ grep 33 hello.s

    3. movl $33, %ebx

  2. 函数内用 static 定义的变量名(i 和 j)在符号表中都加了后缀,主要是方便多个函数定义同样的变量名,因为这些变量仅限该函数内(含多次调用)可见。

  3. 函数内非 static 定义的变量,以及函数参数的传递都是通过 Stack 完成的,这些变量只在函数内(包括 Caller, Callee)可见,外部不可见,所以在符号表中也找不到它们。

  4. 关于函数参数传递,如果明确改变了调用约定,比如函数明确加了 \_\_attribute\_\_((fastcall)) 声明,那么部分参数将通过寄存器传递。不过 main 是例外,因为它的 Caller( __libc_start_main)默认是通过 Stack 传递参数的,再改变它的调用约定就拿不到正确的数据了。

小结

大学的课程蛮多都停留在语法层面的描述,需要透彻理解一些“概念”,还是需要结合实际操作,从终极使用的角度来看这些“概念”,看到的深度和细节会大有不同。

如果想与本文作者更深入地探讨程序链接、装载和运行原理,欢迎订阅吴老师的 10 小时 《360° 剖析 Linux ELF视频课程》,报名链接: 

报名:《360° 剖析 Linux ELF》在线视频课程已经全面上线

转发本文后截图发给 tinylab,赠送该系列 PDF 合集。

(完)


      Linux阅码场原创精华文章汇总

更多精彩,尽在"Linux阅码场",扫描下方二维码关注



你的随手转发或点个在看是对我们最大的支持!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存